package org.codefinger.dao.util.magic;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.codefinger.dao.asm.BaseTypeInfo;
import org.codefinger.dao.asm.ClassWriter;
import org.codefinger.dao.asm.MethodVisitor;
import org.codefinger.dao.asm.Opcodes;
import org.codefinger.dao.util.AbstractCache.ValueBuilder;
import org.codefinger.dao.util.IdentityCache;
import org.codefinger.dao.util.Lang;

public abstract class MagicMethod {

	private static final IdentityCache<Method, MagicMethod>	MAGIC_METHOD_MAP	= new IdentityCache<Method, MagicMethod>(4096, new MagicMethodBuilder());

	protected MagicMethod(Method method) {
		this.method = method;
	}

	protected Method	method;

	public Method getMethod() {
		return method;
	}

	@Override
	public String toString() {
		return method.toString();
	}

	public abstract Object invoke(Object invoker, Object... parameters);

	public static MagicMethod getMagicMethod(Method method) {
		return getMethod(method);
	}

	public static MagicMethod getMagicMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
		try {
			return getMethod(clazz.getMethod(methodName, parameterTypes));
		} catch (NoSuchMethodException e) {
			throw Lang.wrapThrow(e, "Cannot find the specified method.");
		} catch (SecurityException e) {
			throw Lang.wrapThrow(e, "Cannot visit the specified method.");
		}
	}

	public static MagicMethod getMethod(Method method) {
		return MAGIC_METHOD_MAP.get(method);
	}

	private static class MagicMethodBuilder implements ValueBuilder<Method, MagicMethod> {

		@Override
		public MagicMethod build(Method method) {
			int modifiers = method.getModifiers();

			if (!Modifier.isPublic(modifiers)) {
				throw Lang.makeThrow("The method must be public.");
			}

			ClassWriter cw = new ClassWriter();

			String antocreatedClassName = Lang.joinString("CodefingerAutocreatedMagicMethod_", Lang.getLongAdder());
			String superClassName = AsmUtil.getAsmClassName(MagicMethod.class);

			cw.visit(AsmUtil.CODE_VERSION, Opcodes.ACC_PUBLIC, antocreatedClassName, superClassName, null);

			MethodVisitor mv = null;

			// Constructor
			String constructorDesc = AsmUtil.getConstructorDesc(Method.class);
			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", constructorDesc, null, null);
			mv.visitVarInsn(Opcodes.ALOAD, 0);
			mv.visitVarInsn(Opcodes.ALOAD, 1);
			mv.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassName, "<init>", constructorDesc);
			mv.visitInsn(Opcodes.RETURN);
			mv.visitMaxs(2, 2);
			mv.visitEnd();

			// Invoke
			mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "invoke", AsmUtil.getMethodDesc(Object.class, Object.class, Object[].class), null, null);

			String methodClazzName = AsmUtil.getAsmClassName(method.getDeclaringClass());

			Class<?> returnType = method.getReturnType();
			String returnTypeName = returnType.getName();

			boolean flag = returnType.equals(Void.TYPE);

			StringBuilder methodDesc = new StringBuilder("(");
			Class<?>[] parameterTypes = method.getParameterTypes();
			int parameterLength = parameterTypes.length;
			for (Class<?> paramType : parameterTypes) {
				if (!Modifier.isPublic(paramType.getModifiers())) {
					throw Lang.makeThrow("Cannot create MagicMethod for this method '%s', all parameter types must be 'public'.", method);
				}
				methodDesc.append(AsmUtil.getAsmTypeName(paramType));
			}
			methodDesc.append(")");
			if (flag) {
				methodDesc.append("V");
			} else {
				methodDesc.append(AsmUtil.getAsmTypeName(returnType));
			}

			int stackLength = parameterLength + 1;

			boolean staticFlag = Modifier.isStatic(modifiers);

			if (!staticFlag) {
				mv.visitVarInsn(Opcodes.ALOAD, 1);
				mv.visitTypeInsn(Opcodes.CHECKCAST, methodClazzName);
				stackLength++;
			}

			for (int i = 0; i < parameterLength; i++) {

				mv.visitVarInsn(Opcodes.ALOAD, 2);
				mv.visitLdcInsn(i);
				mv.visitInsn(Opcodes.AALOAD);

				Class<?> parameterType = parameterTypes[i];
				String parameterClassName = parameterType.getName();
				String parameterAsmClazzName = AsmUtil.getAsmClassName(parameterType);

				mv.visitTypeInsn(Opcodes.CHECKCAST, parameterAsmClazzName);

				BaseTypeInfo baseTypeInfo = AsmUtil.getBaseTypeInfo(parameterClassName);

				if (baseTypeInfo != null) {
					mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, parameterAsmClazzName, Lang.joinString(parameterClassName, "Value"), Lang.joinString("()", baseTypeInfo.getAsmBaseClassName()));

					if (parameterType.equals(long.class) || parameterType.equals(double.class)) {
						stackLength++;
					}
				}

			}
			mv.visitMethodInsn(staticFlag ? Opcodes.INVOKESTATIC : Opcodes.INVOKEVIRTUAL, methodClazzName, method.getName(), methodDesc.toString());

			BaseTypeInfo baseTypeInfo = AsmUtil.getBaseTypeInfo(returnTypeName);

			if (baseTypeInfo != null) {
				mv.visitMethodInsn(Opcodes.INVOKESTATIC, baseTypeInfo.getAsmPackageClassName(), "valueOf", Lang.joinString("(", baseTypeInfo.getAsmBaseClassName(), ")", baseTypeInfo.getAsmPackageClassType()));
			} else if (flag) {
				mv.visitInsn(Opcodes.ACONST_NULL);
			}
			mv.visitInsn(Opcodes.ARETURN);
			mv.visitMaxs(stackLength, 3);

			try {
				return (MagicMethod) MagicClassLoader.loadByteCodes(antocreatedClassName, cw.toByteArray()).getConstructor(Method.class).newInstance(method);
			} catch (Throwable throwable) {
				throw Lang.wrapThrow(throwable, "It's impossable.");
			}
		}

	}

}
